Custom IPTables extensionsCustom IPTables extensions
Jarosław Sajko
Applying system security policy to firewall rules can be
a difficult task, especially if it requires functionality that the
basic firewall product does not provide. Fortunately, if the firewall
is based on IPTables, you can implement the required functionality
yourself by writing an extension module. What’s more, you’ll be
surprised just how easy it is.
Anyone even vaguely familiar with the Internet and
computers must have heard of firewalls, and anyone who’s ever dealt
with IT security must have configured a software firewall at some
stage. There’s much variety among available firewall systems, but of
course from a technical point of view the crucial factor is the
functionality they offer. Commercial firewall vendors will frequently
do their best to convince you that their software offers unique and
highly advanced features that the competition can only dream of,
provides near-infinite filtering capabilities and comes with an
inexhaustible supply of helpful and cheerful round-the-clock support.
The problem is that support does indeed tend to be
necessary, even though a working product would be much preferable to
delightful e-mail exchanges with the support staff. More often than not
we would also like to see and understand why something is not working,
as it soon turns out that a chosen product’s capabilities are not only
finite, but frequently insufficient. The good news is that if you need
unique and advanced functionality, you can develop it yourself using
open source software such as IPTables, a bit of spare time and
(hopefully) this article. (Which of course is not to say that
commercial packages are useless).
You can download the IPTables package at
www.netfilter.org, and it is supplied as standard with Linux
distributions with kernel 2.4 and later. The website also provides a
brief overview of the project’s history - it’s been around long enough
to be considered a mature and reliable product. In my opinion, one of
the package’s most powerful features is its support for custom
extensions.
The Netfilter package on which IPTables is based is
actually a development framework for filtering and modifying network
packets. From a programmer’s point of view, its key features are:
-
hooks - specific locations in the
system’s network protocol stack from which Netfilter code is called for
each packet that traverses the stack,
-
functions assigned to particular hooks and called by Netfilter for each packet that traverses the protocol stack,
-
the ip_queue driver for queuing packets into user space for asynchronous processing.
The source code is extensively commented - a non-functional, but equally useful feature.
IPTables is essentially a set of modules that use
Netfilter functionality to define rule tables, pattern-based packet
matching criteria and actions to be taken for matching packets. All
this allows Netfilter functionality to be accessed at a higher level of
abstraction for clearer and more convenient use. The name IPTables
originates from the fact that rule lists are represented as tables, and
each named table is stored in memory.
IPTables can generally be divided into two functional
components: one for port and network address translation services
(PNAT) and the other for filtering services. Both components are
extensible. Apart from extension modules, the package also includes
user space tools that allow rules to be entered more conveniently.
Extension modules
An IPTables extension module is a typical kernel module
that has to use a number of standard structures and implement several
module functions. The module code also has to be re-entrant, as it is
possible for its execution to be interrupted during processing by a
request to process another packet. In SMP systems with large numbers of
processors, the chances of interruption are much larger. Here’s a list
of the basic functions that a module has to implement:
-
init_module() -
module entry point, which has the basic task of registering the module
within the framework, returning 0 if successful or a negative integer
in case of failure,
-
cleanup_module() - module exit point; the function code should unregister the module from the Netfilter framework,
-
ipt_register_match() - used to register match extensions; takes a struct ipt_match structure as a parameter,
-
ipt_register_target() - used to register target extensions; takes a struct ipt_target structure as a parameter,
-
ipt_unregister_match() - unregisters a match extension,
-
ipt_unregister_target() - unregisters a target extension.
A module is usually used either as a target module or as
a matching module, so you need to select the four relevant functions
from the collection above. In fact, in actual implementations the
function names are a little different, and we can also make use of
standard macros. The structures passed to ipt_register_match() and
ipt_register_target() are described in the Inset Extension registration
structures.
Apart from the functions listed above, we also need to
implement extension-specific functions, whose pointers were passed in
the module registration structure. Once that’s done, we have a working
module. Let’s see how all this works in practice.
Extension registration structures
Depending on the extension type (match or target), registration is done by calling one of ipt_register_match() or ipt_register_target().
Each function takes a suitable parameter structure (though the two
structure types are similar). The structure fields to be filled in are:
-
name
- extension module name, preferably included in the module file name
(extension module files usually follow the naming convention
ipt_NAME.o),
-
me - a field that should be filled with the THIS_MODULE
literal to indicate the current module; the field is used by the module
reference counter and therefore also the cleanup_module() function,
-
checkentry -
pointer to a function that is called whenever a rule is added for the
current module; the function should check the validity of the new rule,
-
destroy - optional pointer to a function that should be called when removing an entry of the current extension type,
-
match or target (depending
on extension type) - the most important piece of information: a pointer
to a function that matches packets or performs specific operations on
matched packets.
The match structure is called struct ipt_match and the target structure is struct ipt_target.
Implementing a sample extension
Getting back to commercial products for a while, many of
them provide filtering functions that are grouped into packages and
accessible through a few clicks within the GUI. A certain commercial
solution provides a set of simple operations collectively called
Fingerprint Scrambling, which are supposed to hinder operating system
fingerprinting. We will implement similar functionality using the
ipt_TTL, ipt_IPID and ipt_ISN extensions. The first modifies the TTL
field of an IP datagram and is available in the standard package. We
will go through creating the second extension below, and developing the
third one will be left as homework for the Reader.
Operating system fingerprinting
Any attack has to preceded by reconnaissance to gather
information about the target. For attacks in the computer world, vital
information includes the operating system type and version, and target
application versions. Even without local access to the target host, we
can gather information through the analysis of network packets received
from the target system - a process commonly known as OS fingerprinting.
Fingerprinting can be divided into passive and active
methods. Passive fingerprinting is limited to listening for packets
sent by the target system, while active fingerprinting additionally
involves provoking the target to respond by sending it various queries,
attempting to start a TCP session etc.
Once received, the packets are analysed to see how both
obligatory and optional protocol functionality is implemented by the
sender system, and the correlated information is used to determine the
most likely system type and version.
The name of the ipt_IPID extension is less than obvious, so let’s
discuss its purpose first. One of the factors that aid remote OS
fingerprinting is the ability to identify the algorithm used to
calculate the value of the ID field in IP datagrams sent by the remote
system (you can find more information in the Inset ID field in IP
datagrams). Our job will be to modify the ID field in outgoing
datagrams so as to hinder such identification.
ID field in IP datagrams
Each IP datagram header has an ID field, which is used
when reassembling fragmented datagrams. Fragments that are part of the
same datagram have the same unique ID - a 16-bit word, theoretically
allowing up to 65536 packets fragments to be supported by the same node
at any one time. If no fragmentation takes place, the ID field is
basically unused, but operating systems still have to calculate its
value for each packet and use a variety of algorithms for doing so.
Some implementations increment the value by a constant for each
datagram, others increment it by a random value from a limited range,
and still others simply pick a random number for each datagram.
Other system-dependent subtleties also influence the ID
value. A characteristic feature of some older operating systems is that
the ID is always 0 for datagrams with the DF (Don’t Fragment) bit set,
while recent Linux kernel versions always set the ID to 0 for TCP
SYN/ACK packets. And these are just a few of the many
implementation-specific features.
Implementation differences make the ID field very useful
for fingerprinting, and it is analysed both by active scanners (such as
nmap) and passive scanners (such as p0f). To determine the algorithm
used by the target system, simply run nmap with the -v and -O options.
If subsequent IP datagrams for a target system have predictable ID
values, the system can be exploited to scan other networks (including
ones inaccessible from the outside - the technique is called an
idlescan and can also be performed using nmap (using the -sI options).
ipid_checkentry
As already mentioned, IPTables extension modules are
registered by passing a special structure containing several function
pointers. Let’s start by looking at the structure’s checkentry field.
The function indicated by the pointer supplied in this
field is called for each rule that uses the relevant extension. Its
entry parameters are:
-
the name of the table the rule is added to,
-
the rule entry itself, supplied as an ipt_entry structure,
-
extension-specific options,
-
a mask specifying the hooks for which the rule can be called.
A value of 0 is returned if the rule cannot be accepted, and 1 is returned otherwise.
At this stage we can therefore check if the rule is being
added to the right table. Rules that modify packets should be added to
the mangle table. This includes our rule, so can simply check if the
table name is correct. We can also check the validity of
module-specific options, for example verifying whether they fall in a
valid range. If a rule is intended for a specific protocol only, we can
also check the protocol type. Protocol information can be found in the ipt_entry structure.
It is good practice to check whether a structure
containing extension-specific parameters is properly aligned in memory
- the macro to do this is IPT_ALIGN. Our module is fairly simple, so
all we need to check is the memory alignment and the name of the table
the rule is added to. Listing 1 presents the function code.
Listing 1. Code for the ipid_checkentry function
static int ipid_checkentry(const char *tablename,
const struct ipt_entry *e, void *targinfo,
unsigned int targinfosize, unsigned int hook_mask) {
if(strncmp(tablename, "mangle", 6) != 0) {
printk(KERN_WARNING "IPID: Can only be called from the "mangle" table");
return 0;
} if(targinfosize != IPT_ALIGN(sizeof(struct ipt_ipid_target_info))) {
printk(KERN_WARNING "IPID: targinfosize %u != %Zun",
targinfosize, IPT_ALIGN(sizeof(struct ipt_ipid_target_info)));
return 0;
} return 1;
}
ipid_destroy
The function supplied in the destroy field is called
whenever a rule that uses the extension is removed from memory. This
makes it possible to allocate space for rule data in the checkentry
function and release it in the destroy function. In our case the function is empty, so its code will simply be:
static void ipid_destroy(void *targinfo, unsigned int targinfosize) {}
ipid_target
Time to have a look at the function that actually
processes packets. As described above, we can specify either a match or
target function. In our example we’re just modifying the packet and
leaving the job of matching it to other extensions, so we will use
target. A target function takes several parameters, including a pointer
to a socket buffer (skb structure), the input and output interface name
for the packet (one of these can be empty) and rule-specific data. The
data is taken from user space and supplied when the rule is added,
potentially containing extension-specific options or temporary rule
data. We will discuss options and data storage later on, so we’ll skip
the structure for now.
The socket buffer structure is described in detail in the
Inset Socket buffer - all we need to know at this stage is that it’s a
universal Linux kernel structure to allow easy packet manipulation from
all layers of the protocol stack. The socket buffer provides a central
structure for storing the majority of packet-related information, which
will be most useful in developing our extension.
Apart from suitably modifying packet content, the
function also has to inform the IPTables framework what should be done
with the packet. For simple targets such as ACCEPT or DROP, there is
usually just one verdict. For match functions, the verdict usually
specifies whether the packet has been matched or not (although packet
dropping is also possible in error situations).
Our function modifies the packet, so IPTables should be
instructed to accept the packet for further processing. In specific
error situations (such as insufficient memory to process the packet),
we will order the packet to be dropped. The list of possible verdicts
for target functions is short, but quite sufficient:
-
NF_DROP - drop the packet,
-
NF_ACCEPT - accept the packet for further processing,
-
NF_STOLEN - informs Netfilter that the packet along with its entire sk_buff has been taken over by the module,
-
NF_QUEUE - verdict used by Netfilter’s ip_queue module (among others) to queue packets for processing in user space,
-
NF_REPEAT - causes the packet to be re-run through all the functions registered for the current hook.
All these are Netfilter verdicts that we can use, but
since we’re working on a higher level of abstraction (at the IPTables
level), we will use the IPT_CONTINUE verdict, which instructs the framework to continue packet processing and is used by IPTables extensions.
Now we know what the function parameters are, we can set
about implementing the function body. As specified, we will be
modifying the value of the ID field in the IP protocol header. To start
with, we will use a simple algorithm with an internal postincremented
counter. However, regardless of the actual method, we will be changing
the IP header and therefore the content of the socket buffer (struct sk_buff), so we need to notify the system of our intentions. In 2.6 series kernels, this can be done using a single statement:
if (!skb_ip_make_writable(pskb, (*pskb)->len))
return NF_DROP;
We call skb_ip_make_writable()
passing it the buffer and buffer length. Here we also see one situation
where we can order the packet to be dropped - if we cannot process the
packet as required, it will be safer to drop it. In 2.4 series kernels,
the system is notified of the modification by copying the buffer:
struct sk_buff *nskb = skb_copy(*pskb, GFP_ATOMIC);
What we actually to with the data in the buffer (and
therefore in the protocol headers) depends on the purpose of the
extension and on our own inventiveness. The operations are typically
very simple - if no modifications are made, the verdict can often be
determined using a simple comparison. In our case, however, the packet
data is modified, which additionally requires packet checksums to be
updated - the easiest way of doing this is to use the standard
functions sum_fold() and csum_partial(). The code in Listing 2
illustrates the process.
The Netfilter framework should also be notified that
changes have been made to the packet, which is done by setting the
relevant flag in the the socket buffer structure:
(*pskb)->nfcache |= NFC_ALTERED;
A typical requirement for custom extensions is that the
extension module should write notifications to log files. For
performance reasons, logging should not use too much by way of host
resources, so the number of log writes can be limited using
net_ratelimit(). A typical log write call should therefore be something
like:
if(net_ratelimit()) printk("message...\n");
For our first implementation, a simple text message
should suffice. Listing 2 presents a sample implementation of the
target function.
Once the functions are implemented, we just need to
supply a structure filled with the relevant data, register it, add a
few includes and the extension module is ready.
Socket buffer
Apart from user data, network packets also contain
protocol headers. Each protocol stack layer from the transport layer
down prepends its own header to the packet, with the particular layers
and protocols being served by different functions. To avoid having to
copy header information all over the place, a central data structure (a
struct sk_buff) is used to store data about all protocol headers. The
structure contains the following information:
-
packet arrival time (for incoming packets),
-
the network interface that received the packet,
-
checksums,
-
the network socket (if the packet is bound to a local socket),
-
a variety of other data used during packet processing throughout the protocol stack.
The same structure is used by Netfilter and passed to
match and target functions. Several utility functions exist, for
example for copying the structure or making it writeable. You can find
a detailed description of the sk_buff fields in the skbuff.h header file.
Listing 2. Sample implementation for the ipid_target extension
static unsigned int ipid_target(struct sk_buff **pskb,
const struct net_device *in, const struct net_device *out,
unsigned int hooknum, const void *targinfo, void *userinfo) {
struct iphdr * iph;
u_int16_t ipid_diffs[2];
if (!skb_ip_make_writable(pskb, (*pskb)->len))
return NF_DROP;
iph = (*pskb)->nh.iph;
ipid_diffs[0] = (iph->id)^0xffff;
ipid_diffs[1] = iph->id = htons(counter++);
iph->check = csum_fold(csum_partial((char *)ipid_diffs,
sizeof(ipid_diffs), iph->check^0xffff));
(*pskb)->nfcache |= NFC_ALTERED;
return IPT_CONTINUE;
}
User space utility
Once the extension is ready, we need to provide a way of
adding IPTables rules that use it. The rules will be added using the
standard iptables utility, which is also modular, so adding support for
our module is a matter of developing and supplying a suitable library.
The library has to provide the _init() function, from
which the register_match() or register_target() functions will called,
depending on the module type to be registered - very similar to
registering an extension. In our case, we will use register_target(), passing it an argument structure. The most important structure fields are:
-
next - used for creating target lists, for example when listing rules. The initial value should be NULL,
-
name - a name logically related to the library file name, for example IPID for libipt_IPID.so,
-
version - iptables version,
-
help - pointer to a function that displays syntax help for the extension,
-
init - pointer to an optional initialisation function, which will be executed before the function indicated by parse,
-
parse - pointer to a function that parses any parameters unsupported by IPTables itself. If valid options for the extension are supplied, the function should return a non-zero value. One of the entry parameters is invert, which is set to true if the option specification was preceded by a ! character,
-
final_check -
pointer to a function that will be called after extension-specific
options have been parsed, for example allowing exit_error() to be
called if contradictory options are supplied or an obligatory option is
missing,
-
print -
pointer to a function used for displaying non-standard information for
a given rule; called when rules are listed using the iptables -L command,
-
save - pointer to a function used to serialise a rule from memory into a format that can be stored and recreated later,
-
extra_opts -
pointer to a table of structures that constitutes a list of extra
options accepted by the extension (the list should be terminated by a
structure filled with NULLs); the list is merged with the list of standard arguments and passed to getopt_long().
The structure for match extensions is exactly the same.
All we need to do to test and run our module is fill in the argument
structure. For this example, we will declare the functions as bodiless
stubs, and the options list will contain just one record filled with NULLs. Place the compiled library in a location accessible to iptables and we can go on to the testing phase.
Testing
The stability of a kernel module can have an impact on
the stability of the kernel it is loaded into, so it will be safer to
test our module in an isolated sandbox environment. The nfsim utility, also available from the www.netfilter.org website, is an emulator for the Netfilter framwork, and we can use it to safely test new extensions.
Once the emulator is running, we are provided with a
console for entering IPTables rules, generating network packets and
simulating TCP sessions. The effects of operations within the simulator
are displayed on the screen, along with information about each packet’s
progress through the protocol stack and about its final fate. We will
also see any messages returned by the module via the printk() function.
Our extension module should be placed in the
netfilter/ipv4 directory so it is automatically loaded when the
emulator is started. A brief emulator session is usually enough to
discover any critical errors, check that the module does what it
should, verify the checksums etc. The built-in help system (accessed
using the help command) is quite sufficient to learn how to use the emulator, so I will not dwell on the subject here.
Figure 1 presents a typical emulator session. As you can
see, the module was loaded without problems, and its rule was added
correctly. Two packets were then sent. The IPID in the first one was
changed from 0 to 0, and in the second one it was changed from 0 to 1.
That’s because the IPID is always 0 when the packet is generated, but
our module keeps an internal counter that is used as the new IPID and
incremented each time the module is run. Note where in the call chain
our module comes - the rule was added to the FORWARD chain’s mangle
table, so the module message appears before the end of the chain.
Once the extension has been tested in the emulator, we
can try to load the module into a real kernel. So far everything has
been working fine, so we can assume that the alpha release has passed
informal practical testing.
Figure 2 shows four icmp echo-request packets captured
using tcpdump. Each packet appears twice: before and after filtering by
the firewall. You can see the TTL decremented by 1 (which is as it
should be) and a change in the ID, which was introduced by our module -
it only changes by 1, while the original ID changes by more than 1.
Figure 1. Typical session in the nfsim emulator
Figure 2. Packet IDs modified by the firewall
Freedom of choice
Changing the ID as described above might occasionally be
enough to hide our system’s identity, but it won’t always be enough.
Besides, if we wanted to introduce changes to the ID modification
method for the current module version, we would have to change the
source code and recompile the entire module. And what if we wanted
different changes to be made by two different rules that both use our
module? All this can be done by passing parameters from user-space to
the module via the iptables utility. The code to do this is very simple.
Extension options are passed as a list of structures (as usual for getopt()) to the extension’s library file, for example:
static struct option opts[] = {
{"random", 0, 0, 'r'},{"incremental", 1, 0, 'i'},{0}
};
If you’ve ever used the getopt_long() function, you’re no
doubt familiar with the parameter structure fields. If not, refer to
the function’s man page for a detailed description. Once added, the options have to be processed within the parse() function specified for the extension library, again in typical getopt_long() fashion.
Now for the structure to be passed to the kernel. It is,
in fact, the same structure whose alignment we checked in
ipid_checkentry(). For each extension, we need to define a structure
associated with the current rule and used to pass its options. Each
time we need to call a match or target function for an extension, the
same structure will be passed.
However, the structure’s lifetime actually begins when
the rule is added in user space. The structure can be accessed from the
parse() function by dereferencing it from the ipt_entry_target
structure, as shown in Listing 3. The listing also contains the
structure definition.
The flags variable is another important parameter for the parse() function, allowing information to be passed to final_check() and between subsequent parse()
calls. We will use it to inform final_check() which parameters were
supplied by the user. In this case, the user has to specify exactly one
algorithm for modifying the ID field in IP datagrams. Listing 3
presents the sample code. It’s also worth implementing the help(),
print() and save() functions so they provide the required
functionality, but that’s left as an exercise to the Reader.
Listing 3. Implementing extension parameters for an IPTables extension library
struct ipt_ipid_target_info { u_int32_t mode;
u_int32_t step;
};
static int parse(int c, char **argv, int invert, unsigned int *flags,
const struct ipt_entry *entry, struct ipt_entry_target **target) {
struct ipt_ipid_target_info * ipid_info = (struct ipt_ipid_target_info *)(*target)->data;
...
*flags = ipid_info->mode;
return 1;
} static void final_check(unsigned int flags) {
if(!(flags&IPID_MODE_RANDOM) && !(flags&IPID_MODE_INCREMENTAL))
exit_error(PARAMETER_PROBLEM, "You have to choose an algorithmn");
...
}
It is also good practice to use the module’s checkentry() function to verify that the user-supplied options are valid in the first place, but we will not go into it here.
To use one of the options from a kernel module, we can
simply access the structure passed into checkentry() and target() or
match() functions. The structure is accessed in a similar way as for
user space operations.
Listing 4 presents sample code with modified snippets of
the ipid_target() function. After recompiling the module and the
library, we will be able to specify the ID modification method from the
command line.
Listing 4. Accessing extension parameters from kernel-level code
static unsigned int ipid_target(struct sk_buff **pskb,
...
struct ipt_ipid_target_info * ipid_info = (struct ipt_ipid_target_info *)targinfo;
...
if(ipid_info->mode&IPID_MODE_INCREMENTAL) { ipid_diffs[1] = iph->id = htons(counter);
counter += ipid_info->step;
} else if(ipid_info->mode&IPID_MODE_RANDOM) { get_random_bytes(&(ipid_diffs[1]), sizeof(u_int16_t));
ipid_diffs[1] = iph->id = htons(ipid_diffs[1]);
} ...
Data storage
In our example, the counter we use for modifying the ID
value is stored globally. One problem with this approach is that the
counter value is shared by all rules that use the module. It would be
more useful to keep a separate counter for each rule, which would for
example allow packet IDs to be changed randomly for one network and
incrementally for another. How can this be done?
As already mentioned, each call to ipid_target() requires
a targinfo structure to be passed, and the definition of this structure
is entirely up to the programmer. We could therefore add an extra field
to the structure to represent the counter - a separate structure
instance is created for each rule, so we would have a separate counter
for each rule. The modified counter update code in the ipid_target()
function could then be:
if(ipid_info->mode&IPID_MODE_INCREMENTAL) {
ipid_diffs[1] = iph->id = htons(ipid_info->lastval);
ipid_info->lastval += ipid_info->step;
}
However, in solving one problem, we’ve created another.
In SMP systems, each processor will get its own copy of the table, so
we will have to deal with multiple counter copies. We cannot allow the
same value to be repeated, so we need to ensure that only one copy of
the counter can exist, regardless of the number of processors.
One of the simpler ways of solving the problem is to add
an extra field to the targinfo structure, containing a pointer to the
master copy of the structure. We can then use the pointer to reference
fields whose values are modified. Adding this to the code requires only
cosmetic changes - apart from adding the extra field to targinfo, we
just need to add the following assignment to the ipid_checkentry()
function:
ipid_info->master = ipid_info;
and change all references to the last counter value within ipid_target from:
ipid_info->lastval
to:
ipid_info->master->lastval
Apart from the tables being copied, we also have one
other problem to solve: the module code has to be re-entrant, as it is
possible for packet processing to be interrupted by a request to
process another packet. Wherever concurrent data access appears, the
programmer has to manage it to ensure data consistency. Suppose we have
two packets being processed concurrently, and the counter value is read
for both of them before the counter is incremented - such behaviour
would result in two packets having the same IDs, and other
inconsistencies might appear as well.
An easy solution to the problem is to protect the counter
using a spinlock_t. Spin locks are a kernel-level synchronisation
mechanism. To use a lock, we start by declaring it:
static spinlock_t ipid_lock = SPIN_LOCK_UNLOCKED;
From now on, whenever we refer to a shared value, we can
lock the spin lock before accessing the value, using a function call
like:
spin_lock_bh(&ipid_lock);
and remove the lock once the protected operation is complete:
spin_unlock_bh(&ipid_lock);
Locking and unlocking calls should be coordinated so as
to prevent a situation where one thread wants to get a lock, but has to
wait forever because another thread is waiting to remove the lock. This
would result in a deadlock situation, leaving the entire system dead as
a dodo.
You might think we’ve addressed all possible data storage
requirements for our extension, but in fact you don’t have to look far
for a situation where they won’t be sufficient. Suppose we wanted to
keep separate counters for each IP stream, defined as a (source,
target) pair. We would then most likely need to create more elaborate
data structures - while it would theoretically be possible to introduce
a new rule for each pair, this would be highly inefficient.
In such cases, you might consider creating custom object
caches and using more elaborate data structures for your modules (hash
tables, trees, lists), but that’s a story for another article
altogether.
The end is just the beginning
With the information provided in this article and a bit
of time, you should now be able to write a simple but fully functional
extension module, and have a solid basis for exploring the subject on
your own and writing some more advanced modules.
The IPTables package comes with numerous modules which
have not been mentioned here and make use of more advanced framework
functionality, such as connection tracking, filtering for protocols
that use parallel connections, and advanced network address translation.
The ipt_IPID module we’ve just completed can be used as
one of the components of a firewall installation to conceal the true
identity of protected operating systems. While on its own it is
obviously quite insufficient to protect a system from fingerprinting
(just like the Fingerprint Scrambling function of a certain commercial
firewall solution), it can be quite effective in preventing the
protected hosts from being used to explore the network.
Remember, though, that the ID field is crucial for
fragmented IP packets, so it cannot be modified for packets that are
actually datagram fragments. This means that the rule should be skipped
for fragmented packets, which can be done for example by writing a
suitable match extension.
Developing the ipt_ISN module mentioned earlier in the
article is left as a longer exercise for the reader, as it requires
several new problems to be solved, including storing state information
for TCP connections and ensuring bilateral sequence number modification
(SEQ packets are affected in one direction and ACK packets in the
other). However, the problems can quite definitely be solved, which I
can personally vouch for since I actually created such a module while
working on this article.